package disjunction.extensions.view
{
	import flash.display.Bitmap;
	import flash.display.BitmapData;
	import flash.geom.Point;
	import flash.geom.Rectangle;

	/**
	 * Encapsulates read and write access to the various discrete frames of a spritesheet.
	 * Frames are categorised by animation, base frame, and rotational subframe.
	 */
	//TODO probably should extend, since it is an "IS-A" relationship.
	public class BitmapDataSpriteSheet //extends BitmapData
	{
		/** Total pixel capacity for any BitmapData object. */
		private const MAX_PIXELS:int = 16777216;
		
		/** The pixel width of the BitmapData object. Keeping the width as wide as is
		 * allowed by BitmapData improves speed by ensuring that there are less vertical
		 * iterations on the cursor (in many cases, only one row will ever be needed for
		 * all frames of the spritesheet). 
		 */
		private const MAX_WIDTH:uint = 8191; //0 to 8191 = 8192 units
		
		/** The pixel width of the BitmapData object. **/
		private const MAX_HEIGHT:uint = MAX_PIXELS / MAX_WIDTH; //0 to 8191 = 8192 units
		
		private const ERROR_FRAME_SIZE_OUT_OF_RANGE:String = "Frame dimensions must be positive and less than (MAX_WIDTH, MAX_HEIGHT).";
		private const ERROR_FRAME_SIZE_INCONGRUOUS:String = "Frame dimensions do not match constructor-specified values.";
		private const ERROR_NUM_ROTATIONS_OUT_OF_RANGE:String = "Animations must contain at least one rotation per base frame.";
		private const ERROR_NUM_FRAMES_OUT_OF_RANGE:String = "Animations must contain at least one frame (base frame).";
		private const ERROR_NO_SUCH_ANIMATION:String = "No such animation.";
		private const ERROR_CANNOT_SUPPORT_NUMBER_OF_FRAMES:String = "BitmapData storage capacity exceeded.";
		/**
		 * @private Stores frames as a single spritesheet.
		 * Remember, BitmapData can only be 16,777,215 pixels total
		 * 8192 is max width. So eg. 4096x4096 or 8192x2048.
		 */
		private var _bitmapData:BitmapData;
		
		/** @private Stores each named frame's x,y position in bmd, by string key. */
		private var _animations:Object = {};
	
		/** Number of rotations per each base frame. */
		private var _numRotations:uint = 0;
		
		/** @private The correctly-sized selection rectangle for this sprite's frames. */
		private var _writeCursor:Rectangle = new Rectangle();
		private var _sourceRect:Rectangle = new Rectangle();
		private var _origin:Point = new Point();
		
		/**
		 * Constructor. All parameters exist so as to allow one-off allocation of a suitably-sized
		 * BitmapData instance to support this spritesheet.
		 * @param frameWidth The width of each frame in the spritesheet.
		 * @param frameHeight The height of each frame in the spritesheet.
		 * @param numBaseFrames The number of *base* frames, i.e. frames without any rotation.
		 * @param numRotations The number of rotations, including the base frame rotation. @default 1
		 */

		public function BitmapDataSpriteSheet(frameWidth:uint, frameHeight:uint, numBaseFrames:uint, numRotations:uint = 1)
		{			
			//sanity checks
			if (frameWidth <= 0 ||
				frameWidth > MAX_WIDTH ||
				frameHeight <= 0 ||
				frameHeight > MAX_HEIGHT)
				throw new Error(ERROR_FRAME_SIZE_OUT_OF_RANGE);

			if (numBaseFrames < 1)
				throw new Error(ERROR_NUM_FRAMES_OUT_OF_RANGE);

			if (numRotations < 1)
				throw new Error(ERROR_NUM_ROTATIONS_OUT_OF_RANGE);
			
			_numRotations = numRotations;
			
			_writeCursor.width = _sourceRect.width = frameWidth;
			_writeCursor.height = _sourceRect.height = frameHeight;
			//offset by one width to allow for first cursor advance
			_writeCursor.x = -frameWidth;
			
			//check how much capacity is needed: max row width is fixed so ultimately
			//it comes down to how many rows the bitmapdata can then support.
			var framesNeeded:uint = numBaseFrames * numRotations;
			var widthNeeded:uint = frameWidth * framesNeeded;
			var rowsNeeded:uint = Math.ceil(widthNeeded / MAX_WIDTH);
			var heightNeeded:uint = frameHeight * rowsNeeded;
			
			if (heightNeeded <= MAX_HEIGHT) //then feasible
			{
				trace("======", widthNeeded, heightNeeded);
				_bitmapData = new BitmapData(widthNeeded, heightNeeded, true, 0);
			}
			else throw new Error(ERROR_CANNOT_SUPPORT_NUMBER_OF_FRAMES);
		}
		
		public function drawFrame(animationName:String, frame:uint, rotationIndex:uint, destination:BitmapData, position:Point):void
		{
			destination.copyPixels(_bitmapData,_animations[animationName][frame][rotationIndex], position);
		}
		
		/**
		 * Adds a frame to an animation, as per number of rotations specified in the
		 * constructor. If the animation does not exist, it is created. Rotations
		 * are given indices based on increasing clockwise order of rotation as they
		 * are created; the base frame is always at index 0.
		 * 
		 * @param frameData The BitmapData instance representing the base frame, to add.
		 * This must have dimensions equal to those specified in the constructor.
		 * @param animationName The name of the animation to add the frame to.
		 */
		public function addAnimationFrame(animationName:String, frameData:BitmapData):void
		{	
			if (frameData.width != _sourceRect.width || frameData.height != _sourceRect.height)
				throw new Error(ERROR_FRAME_SIZE_INCONGRUOUS);

			//ensure we have the data structures set up to store frame positions
			var animation:Vector.<Vector.<Rectangle>>;
			if (!_animations.hasOwnProperty(animationName))
				_animations[animationName] = new Vector.<Vector.<Rectangle>>();
			animation = _animations[animationName];
			var frame:Vector.<Rectangle> = new Vector.<Rectangle>();
			animation.push(frame);
			
			for (var i:uint = 0; i < _numRotations; i++)
			{
				advanceWriteCursor();
				
				if (i > 0) //rotate if not base
				{
					frameData = rotateFrame(frameData, (Math.PI * i) / _numRotations);
				}
				
				frame.push(_writeCursor.clone());
				_bitmapData.copyPixels(frameData, _sourceRect, _writeCursor.topLeft);
			}
		}
		
		private function rotateFrame(frameData:BitmapData, rotationRad:Number):BitmapData
		{
			//TODO
			//Check this code out from SVN BitmapData rotation tests
			throw new Error("NOT IMPLEMENTED");
		}
		
		private function advanceWriteCursor():void
		{
			//shift horizontally
			_writeCursor.x += frameWidth;
			
			//if we jumped the right bound out of horizontal range
			if (_writeCursor.right > MAX_WIDTH)
			{
				//do a "carriage return"!
				_writeCursor.x = 0;
				_writeCursor.y += frameHeight;
				
				//can't store any more given BitmapData's intrinsic size limit.
				if (_writeCursor.bottom > MAX_HEIGHT)
					throw new Error("Frame storage is at maximum capacity.");
			}
		}
				
		public function get frameWidth():uint
		{
			return _sourceRect.width;
		}
		
		public function get frameHeight():uint
		{
			return _sourceRect.height;
		}
		
		public function numFramesInAnimation(animationName:String):uint
		{
			if (!hasAnimation(animationName))
				throw new Error(ERROR_NO_SUCH_ANIMATION);
			return _animations[animationName].length;
		}
		
		public function get numRotations():uint
		{
			return _numRotations;
		}
		
		public function hasAnimation(animationName:String):Boolean
		{
			return _animations.hasOwnProperty(animationName);
		}
		
	}
}